﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using GalaSoft.MvvmLight.Messaging;
using HtmlAgilityPack;

namespace SteamGroupPlayers.Model
{
	#region GAME COMPARER
	class GameComparer : IEqualityComparer<Game>
	{
		public bool Equals(Game x, Game y)
		{
			if (Object.ReferenceEquals(x, y))
				return true;
			if (Object.ReferenceEquals(x, null) || Object.ReferenceEquals(y, null))
				return false;

			return x.AppID == y.AppID;
		}

		public int GetHashCode(Game game)
		{
			if (Object.ReferenceEquals(game, null))
				return 0;

			return game.AppID == 0 ? 0 : game.AppID.GetHashCode();
		}
	} 
	#endregion

	#region EVENT ARGS
	public class GroupDataProgressEventArgs : EventArgs
	{
		public string Group { get; private set; }
		public double Progress { get; private set; }

		public GroupDataProgressEventArgs(string group, double progress)
		{
			Group = group;
			Progress = progress;
		}
	}

	public class GroupPlayersLoadedEventArgs : EventArgs
	{
		public List<Game> Games { get; private set; }

		public GroupPlayersLoadedEventArgs(List<Game> games)
		{
			Games = games;            
		}

		public GroupPlayersLoadedEventArgs(HashSet<Game> games)
		{
			Games = games.ToList();
		}
	}

	public class GroupDataLoadedEventArgs : EventArgs
	{
		public bool HasError { get; private set; }
		public string Name { get; private set; }
		public string Avatar { get; private set; }
		public List<Player> Players { get; private set; }

		public GroupDataLoadedEventArgs()
		{
			HasError = true;
		}

		public GroupDataLoadedEventArgs(string name, string avatar, List<Player> players)
		{
			Name = name;
			Avatar = avatar;
			Players = players;
		}
	} 
	#endregion

	/// <summary>
	/// Responsible to communicate with Steam (Web API and simple web pages).
	/// </summary>
	public class Loader : IDisposable
	{
		#region Fields
		private CancellationTokenSource _tokenSource = new CancellationTokenSource();
		private bool _running = false;
		#endregion

		#region Events
		public delegate void GroupDataLoadedEventHandler(object sender, GroupDataLoadedEventArgs e);
		public delegate void GroupPlayersLoadedEventHandler(object sender, GroupPlayersLoadedEventArgs e);
		public delegate void CanceledEventHandler(object sender, EventArgs e);
		public delegate void GroupDataProgressHandler(object sender, GroupDataProgressEventArgs e);

		public event GroupDataLoadedEventHandler OnGroupDataLoaded;
		public event GroupPlayersLoadedEventHandler OnPlayersLoaded;
		public event CanceledEventHandler OnCancel;
		public event GroupDataProgressHandler OnGroupDataProgress;
		#endregion

		#region Methods
		public void Dispose()
		{
			Cancel();
			_tokenSource.Dispose();
		}

		public void Cancel()
		{
			_tokenSource.Cancel();

			if (OnCancel != null)
				OnCancel(this, new EventArgs());

			_tokenSource = new CancellationTokenSource();
		}

		/// <summary>
		/// Used to kick off group Loading. Use the classes Event-Handlers to retrieve the operation's results.
		/// </summary>
		/// <param name="group">Short name of the group</param>
		/// <param name="isGame">Official game group or standard community group?</param>
		public void LoadGroup(string group, bool isGame = false)
		{
			Cancel(); // in case, we are already loading a group

			Task.Factory.StartNew(() => DoLoadGroup(group, isGame), _tokenSource.Token);
		}

		/// <summary>
		/// Simplistic helper to download string data from a given url. Just in case, we need it more often.
		/// </summary>
		/// <param name="url">Url to download data from</param>
		/// <returns>String data</returns>
		private string Download(string url)
		{
			using (StreamReader sr = new StreamReader(HttpWebRequest.Create(url).GetResponse().GetResponseStream()))
			{
				return sr.ReadToEnd();
			}
		}

		private Tuple<string, string, List<Player>> GetGroupData(string url)
		{
			XDocument xdoc = XDocument.Load(url);

			string groupName = xdoc.Descendants("groupName").Select(n => n.Value).FirstOrDefault();
			string imageURL = xdoc.Descendants("avatarFull").Select(n => n.Value).FirstOrDefault();
			List<Player> players = xdoc.Descendants("steamID64").Select(n => new Player { ID = long.Parse(n.Value) }).ToList();

			double page = xdoc.Descendants("currentPage").Select(n => int.Parse(n.Value)).FirstOrDefault();
			double pages = xdoc.Descendants("totalPages").Select(n => int.Parse(n.Value)).FirstOrDefault();

			if(OnGroupDataProgress != null)
				OnGroupDataProgress(this, new GroupDataProgressEventArgs(groupName, page / pages));

			string nextPage = xdoc.Descendants("nextPageLink").Select(n => n.Value).FirstOrDefault();
			if (!String.IsNullOrEmpty(nextPage))
				players.AddRange(GetGroupData(nextPage).Item3);

			return new Tuple<string, string, List<Player>>(groupName, imageURL, players);
		}

		private void DoLoadGroup(string group, bool isGame)
		{
			try
			{
				string URLgroup = isGame ? SteamGroupPlayers.Properties.Resources.GameURL : SteamGroupPlayers.Properties.Resources.GroupURL;
				string url = URLgroup.Replace("%GROUP%", group);
				Tuple<string, string, List<Player>> groupData = GetGroupData(url);

				if (OnGroupDataLoaded != null)
					OnGroupDataLoaded(this, new GroupDataLoadedEventArgs(groupData.Item1, groupData.Item2, groupData.Item3));

				// Download each and every player's info
				Parallel.ForEach(groupData.Item3, new ParallelOptions { MaxDegreeOfParallelism = 10, CancellationToken = _tokenSource.Token }, DoLoadPlayer);
				// Generate a list of distinct games from the loaded players
				List<Game> games = groupData.Item3.Where(p => p.Played != null).SelectMany(p => p.Played).Distinct(new GameComparer()).OrderBy(g => g.Name).ToList();

				if (OnPlayersLoaded != null)
					OnPlayersLoaded(this, new GroupPlayersLoadedEventArgs(games));
			}
			catch (OperationCanceledException ocEx)
			{
				// Usually this should only happen, when we want it to happen . . . so just ignore this error.
			}
			catch (Exception ex)
			{
				// Something went wrong, so we send an Exception-Message and also raise the GroupDataLoadedEvent (with HasError = true)
				Messenger.Default.Send<Exception>(ex);
				if (OnGroupDataLoaded != null)
					OnGroupDataLoaded(this, new GroupDataLoadedEventArgs());
			}
		}

		private void DoLoadPlayer(Player p)
		{
			string url;
			XDocument xdoc;

			try
			{
				if (String.IsNullOrEmpty(p.CommunityURL))
					p.CommunityURL = Properties.Resources.UserURL.Replace("%PLAYER%", p.ID.ToString());

				//general player data
				url = p.CommunityURL + "?xml=1";
				xdoc = XDocument.Load(url);

				p.Name = (from name in xdoc.Descendants("steamID")
						  select name.Value).FirstOrDefault().Replace("\r\n", "");

				p.PrivacyState = (from pState in xdoc.Descendants("privacyState")
								  select pState.Value).FirstOrDefault();

				p.Public = !(String.IsNullOrEmpty(p.PrivacyState) || p.PrivacyState != "public");

				p.State = (from state in xdoc.Descendants("onlineState")
						   select state.Value).FirstOrDefault();

				p.StateMessage = (from stateMessage in xdoc.Descendants("stateMessage")
								  select stateMessage.Value).FirstOrDefault();
				if (p.StateMessage != null)
					p.StateMessage = p.StateMessage.Replace("<br />", "\r\n");

				p.AvatarIcon = (from ava in xdoc.Descendants("avatarIcon")
								select ava.Value).FirstOrDefault();

				p.AvatarMedium = (from ava in xdoc.Descendants("avatarMedium")
								  select ava.Value).FirstOrDefault();

				p.AvatarFull = (from ava in xdoc.Descendants("avatarFull")
								select ava.Value).FirstOrDefault();

				p.VACBanned = (from vac in xdoc.Descendants("vacBanned")
							   select vac.Value).FirstOrDefault() != "0";

				//game data
				url = SteamGroupPlayers.Properties.Resources.PlayerURL.Replace("%PLAYER%", p.ID.ToString());
				//xdoc = XDocument.Load(url);
				using (WebClient cli = new WebClient())
				{
					string data = cli.DownloadString(url);

					if (data.StartsWith("<!DOCTYPE HTML PUBLIC")) // XML-Data not available => Profile not public
						throw new Exception(String.Format("Profile of '{0}' is not public", p.Name));

					xdoc = XDocument.Parse(data);

					p.Name = (from name in xdoc.Descendants("steamID")
							  select name.Value).FirstOrDefault().Replace("\r\n", "");

					p.Played = (from game in xdoc.Descendants("game")
								select new Game
								{
									Name = game.Element("name") != null ? game.Element("name").Value : "",
									StoreLink = game.Element("storeLink") != null ? game.Element("storeLink").Value : "",
									Logo = game.Element("logo") != null ? game.Element("logo").Value : "",
									AppID = game.Element("appID") != null ? int.Parse(game.Element("appID").Value) : 0,
									PlayedAll = game.Element("hoursOnRecord") != null ? double.Parse(game.Element("hoursOnRecord").Value, System.Globalization.CultureInfo.InvariantCulture) : 0,
									PlayedRecent = game.Element("hoursLast2Weeks") != null ? double.Parse(game.Element("hoursLast2Weeks").Value, System.Globalization.CultureInfo.InvariantCulture) : 0
								}).OrderByDescending(g => g.PlayedRecent).ThenByDescending(g => g.PlayedAll).ToList();

					p.SumAll = p.Played.Sum(g => g.PlayedAll);
					p.SumRecent = p.Played.Sum(g => g.PlayedRecent);
				}
			}
			catch (Exception ex)
			{
				Messenger.Default.Send<Exception>(ex);

				p.IsCorrupted = true;
			}
			finally
			{
				p.IsLoading = false;

				// Tell the application, that we just finished loading a player (could be used in View)
				Messenger.Default.Send<int>(0, "PlayerQueue");
			}
		} 
		#endregion
	}
}
